﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using OMC.Classification;
using Outlook = Microsoft.Office.Interop.Outlook;
using System.Threading;
using Settings = OMC.Properties.Settings;
using System.Runtime.InteropServices;
using System.Diagnostics;
using Microsoft.Practices.EnterpriseLibrary.Logging;
using System.Runtime.Serialization.Formatters.Binary;
using System.IO;
using weka.core;
using System.Runtime.Serialization;
using OMC.Utils;
using weka.classifiers;
using weka.classifiers.bayes;
using System.Windows.Forms;

namespace OMC.DataManagement
{
    /// <summary>
    /// An object of this class can manage all the things about the
    /// handling of the Add-In.
    /// </summary>
    public class Manager
    {
        private const String folderListFilename = "folderList";
        private List<Folder> folderList;
        public List<Folder> FolderList
        {
            get { return folderList; }
            private set { folderList = value; }
        }

        private bool blocking;
       
        private List<IgnoreListItem> ignoredFolders;
        private Outlook.Stores stores;
        public Outlook.NameSpace nameSpace;

        public TextClassifierBuilder textClassifierBuilder;
        private const String textClassifierBuilderFilename = "builder";

        public TextClassifier textClassifier;
        private const String textClassifierFilename = "classifier";

        private Evaluation evaluation;
        private Evaluation Evaluation { get { if (evaluation == null) { evaluation = textClassifierBuilder.Evaluate(); } return evaluation; } set { evaluation = value; } }

        private List<Outlook.Folder> FoldersToKeepAlive = new List<Outlook.Folder>();

        // shall the classifier be updated whenever a mail is moved from/to a folder?
        // This feature needs a classifier that implements the UpdateableClassifier interface.
        // Additionally it must accept calls to updateClassifier(instance) whereas the instance has negative weights.
        // This is the case for the NaiveBayesMultinomialUpdateable classifier (versions after 2012-12-30)
        private bool isClassifierToBeUpdated = true;

        /// <summary>
        /// The constructor to create an object.
        /// </summary>
        /// 
        /// <param name="stores">actual outlook stores</param>
        /// <param name="nameSpace">actual outlook namespace</param>
        public Manager(Outlook.Stores stores, Outlook.NameSpace nameSpace, bool blocking)
        {
            this.stores = stores;
            this.nameSpace = nameSpace;
            this.blocking = blocking;
            this.init();
        }

        /// <summary>
        /// This method loads the whole dataset after loading the properties.
        /// </summary>
        public void init()
        {
            Logger.Write("Initialize Manager");
            //Directory.CreateDirectory(Main.ApplicationDataPath);
            Thread thread1 = new Thread(StartCachingThread);
            thread1.Start();
            Thread thread2 = new Thread(StartCallbackRegisteringThread);
            thread2.Start();
            Logger.Write("Finished initializing Manager");
            if (this.blocking == true)
            {
                thread1.Join();
                thread2.Join();
            }
        }



        /// <summary>
        /// Refreshes the content of folderList of type List&lt;Folder>
        /// </summary>
        /// <returns>noting</returns>
        public void RefreshFolderList()
        {
            this.folderList = new List<Folder>();
            foreach (Outlook.Store store in stores)
            {
                if (System.Environment.UserName == "Martin Studer" && store.DisplayName != "Martin.Studer@htwchur.ch")
                {
                    continue;
                }
                try
                {
                    Outlook.Folder rootFolder = store.GetRootFolder() as Outlook.Folder;
                    IndexFolder(rootFolder, folderList);
                }
                catch (COMException ex)
                {
                    String message = "It isn't possible to load mail data of Store";
                    LoggerUtils.Write(message, ex, TraceEventType.Error);
                }
            }
        }

        /// <summary>
        /// This function is recursively called and traverses the given folder "rootFolder" and its subfolders.
        /// For every encountered folder a callback function to the event BeforeItemMove gets attached.
        /// IsFolderToBeProcessed() will not be used here: We want to be notified when a mail gets moved out of e.g. the inbox
        /// </summary>
        /// <param name="rootFolder">the starting point for the recursion</param>
        private void RegisterCallbacks(Outlook.Folder rootFolder)
        {
            if (isClassifierToBeUpdated)
            {
                String rootFolderName = "";
                String rootFolderPath = "";

                // get root folder name and path (used for error messages)
                try
                {
                    rootFolderName = rootFolder.Name;
                    rootFolderPath = rootFolder.FolderPath;
                }
                catch (COMException ex)
                {
                    String message = "Cannot access root folder name or path.";
                    LoggerUtils.Write(message, ex, TraceEventType.Critical);
                }

                FoldersToKeepAlive.Add(rootFolder);
                rootFolder.BeforeItemMove += new Outlook.MAPIFolderEvents_12_BeforeItemMoveEventHandler(rootFolder_BeforeItemMove);
                //MessageBox.Show("RegisterCallbacks() called for: " + rootFolder.Name, "Info", MessageBoxButtons.OK, MessageBoxIcon.None); // alert each processed folder
                
                try
                {
                    foreach (Outlook.Folder subfolder in rootFolder.Folders)
                    {
                        RegisterCallbacks(subfolder);
                    }
                }
                catch (COMException ex)
                {
                    String message = "Cannot access subfolders of folder: " + rootFolderName + " in function RegisterCallbacks(). ";
                    LoggerUtils.Write(message, ex, TraceEventType.Critical);
                }
            }
        }

        /// <summary>
        /// This function is recursively called and traverses the given folder "rootFolder" and its subfolders.
        /// Every encountered folder is added to the provided "folderList".
        /// For a folder to be used, IsFolderToBeProcessed() must return true
        /// </summary>
        /// <param name="rootFolder">the starting point for the recursion</param>
        /// <param name="folderList">encountered folders are added here</param>
        public void IndexFolder(Outlook.Folder rootFolder, List<Folder> folderList)
        {
            String rootFolderName = "";
            String rootFolderPath = "";

            // get root folder name and path (used for error messages)
            try
            {
                rootFolderName = rootFolder.Name;
                rootFolderPath = rootFolder.FolderPath;
            }
            catch (COMException ex)
            {
                String message = "Cannot access root folder name or path.";
                LoggerUtils.Write(message, ex, TraceEventType.Critical);
            }

            // do recursion
            if (IsBrowsingSubfoldersAllowed(rootFolder))
            {
                try
                {
                    foreach (Outlook.Folder subfolder in rootFolder.Folders)
                    {
                        IndexFolder(subfolder, folderList);
                    }
                }
                catch (COMException ex)
                {
                    String message = "Cannot access subfolders of folder: " + rootFolderName + " in function IndexFolder(). ";
                    LoggerUtils.Write(message, ex, TraceEventType.Critical);
                }
            }

            if (!IsFolderToBeProcessed(rootFolder))
            {
                return;
            }


            // process mails in this folder (=rootFolder)
            try
            {
                Folder omcFolder = new Folder(rootFolder.Name, rootFolder.EntryID, rootFolder.StoreID);
                folderList.Add(omcFolder);
                foreach (object oItem in rootFolder.Items)
                {
                    if (!(oItem is Outlook.MailItem))
                    {
                        continue;
                    }

                    string subject = "not assigned";
                    try
                    {
                        Outlook.MailItem item = oItem as Outlook.MailItem;
                        subject = item.Subject; // get it first in case of crash
                        omcFolder.AddMailContent(item.Body, subject, item.SenderEmailAddress, item.To);
                    }
                    catch (COMException ex)
                    {
                        //log file entry
                        String message = "It isn't possible to index mail, mail subject:\n"
                                        + subject + "\nin folder:\n" + rootFolderPath;
                        LoggerUtils.Write(message, ex, TraceEventType.Critical);
                    }
                }
            }
            catch (COMException ex)
            {
                String message = "Problem while processing mails of folder: " + rootFolderName;
                LoggerUtils.Write(message, ex, TraceEventType.Critical);
            }
        }

        
        /// <summary>
        /// This function is called whenever an item (mail, calendar entry, etc.) is moved out of one of the registered folders.
        /// </summary>
        /// <param name="Item">a movable Outlook item</param>
        /// <param name="MoveTo">the folder to which the item is going to be moved</param>
        /// <param name="Cancel">change it if you don't want the mail to be moved</param>
        public void rootFolder_BeforeItemMove(object Item, Outlook.MAPIFolder MoveTo, ref bool Cancel)
        {
            Outlook.MailItem mail = Item as Outlook.MailItem;
            if (mail != null && textClassifierBuilder != null && textClassifier != null) { // is Item of type Outlook.MailItem?
                MailContent mailContent = new MailContent(mail.Body, mail.Subject, mail.SenderEmailAddress, mail.To); //TODO: detour via MailContent is taken to prevent to implement the getContent function twice ; can we do it better?
                //MessageBox.Show("BeforeItemMove() fired: Mail subject is: " + mail.Subject + "\nmail.Parent.EntryId is: " + mail.Parent.EntryId, "Info", MessageBoxButtons.OK, MessageBoxIcon.None);

                if (textClassifierBuilder.IsFolderExisting(mail.Parent.EntryId + " " + mail.Parent.StoreId))
                {
                    //MessageBox.Show("Going to unadd mail from old folder (" + mail.Parent.Name + ").", "Info", MessageBoxButtons.OK, MessageBoxIcon.None);
                    //textClassifierBuilder.SaveUnfiltered("C:\\Users\\toggenblukas\\AppData\\Roaming\\Outlook Mail Classifier\\1_unfiltered.arff");
                    //textClassifierBuilder.SaveFiltered("C:\\Users\\toggenblukas\\AppData\\Roaming\\Outlook Mail Classifier\\1_filtered.arff");
                    //textClassifier.saveClassifier("C:\\Users\\toggenblukas\\AppData\\Roaming\\Outlook Mail Classifier\\1.model");
                    textClassifierBuilder.AddTrainingInstance(mailContent.GetContent(), mail.Parent.EntryId + " " + mail.Parent.StoreId, -1);
                    textClassifier.updateClassifier(textClassifierBuilder.Text2FilteredInstance(mailContent.GetContent(), mail.Parent.EntryId + " " + mail.Parent.StoreId, -1));
                    //textClassifierBuilder.SaveUnfiltered("C:\\Users\\toggenblukas\\AppData\\Roaming\\Outlook Mail Classifier\\2_unfiltered.arff");
                    //textClassifierBuilder.SaveFiltered("C:\\Users\\toggenblukas\\AppData\\Roaming\\Outlook Mail Classifier\\2_filtered.arff");
                    //textClassifier.saveClassifier("C:\\Users\\toggenblukas\\AppData\\Roaming\\Outlook Mail Classifier\\2.model");
                }
                else
                {
                    //MessageBox.Show("BeforeItemMove wanted to unadd mail from old folder (" + mail.Parent.Name + "), but its not registered in textClassifierBuilder.", "Info", MessageBoxButtons.OK, MessageBoxIcon.None);
                    Logger.Write("BeforeItemMove() was called to unadd mail from old folder (" + mail.Parent.Name + ") but hasn't got an attribute value in the TextClassifierBuilder. This should never happen!");
                }
                if (MoveTo != null)
                {
                    if (textClassifierBuilder.IsFolderExisting(MoveTo.EntryID + " " + MoveTo.StoreID))
                    {
                        //MessageBox.Show("Going to add mail to new folder (" + MoveTo.Name + ")", "Info", MessageBoxButtons.OK, MessageBoxIcon.None);
                        //textClassifierBuilder.SaveUnfiltered("C:\\Users\\toggenblukas\\AppData\\Roaming\\Outlook Mail Classifier\\3_unfiltered.arff");
                        //textClassifier.saveClassifier("C:\\Users\\toggenblukas\\AppData\\Roaming\\Outlook Mail Classifier\\3.model");
                        //textClassifierBuilder.SaveFiltered("C:\\Users\\toggenblukas\\AppData\\Roaming\\Outlook Mail Classifier\\3_filtered.arff");
                        textClassifierBuilder.AddTrainingInstance(mailContent.GetContent(), MoveTo.EntryID + " " + MoveTo.StoreID, 1);
                        textClassifier.updateClassifier(textClassifierBuilder.Text2FilteredInstance(mailContent.GetContent(), MoveTo.EntryID + " " + MoveTo.StoreID, 1));
                        //textClassifierBuilder.SaveUnfiltered("C:\\Users\\toggenblukas\\AppData\\Roaming\\Outlook Mail Classifier\\4_unfiltered.arff");
                        //textClassifierBuilder.SaveFiltered("C:\\Users\\toggenblukas\\AppData\\Roaming\\Outlook Mail Classifier\\4_filtered.arff");
                        //textClassifier.saveClassifier("C:\\Users\\toggenblukas\\AppData\\Roaming\\Outlook Mail Classifier\\4.model");
                    }
                    else
                    {
                        //MessageBox.Show("BeforeItemMove() was called to move a mail to a new folder (" + MoveTo.Name + ") which is not configured to be used by OMC, e.g. the inbox.", "Info", MessageBoxButtons.OK, MessageBoxIcon.None);
                        Logger.Write("BeforeItemMove() was called to move a mail to a new folder " + MoveTo.Name + ". This can happen when a mail was moved to a folder which is not configured to be used by OMC.");
                    }
                }
            }
        }
        

        /// <summary>
        /// This method checks the folder default item type and if the folder is type of default folders.
        /// </summary>
        /// <param name="folder">in folder to check</param>
        /// <returns>
        /// true, if folder could be used
        /// false, if folder should be banned
        /// </returns>
        private bool IsFolderToBeProcessed(Outlook.Folder folder)
        {
            if (folder.DefaultItemType != Outlook.OlItemType.olMailItem)
            {
                return false;
            }

            foreach (IgnoreListItem ignoredFolder in ignoredFolders)
            {
                if (ignoredFolder.getName() == folder.Name.ToLower())
                {
                    return false;
                }
            }
            return true;
        }

        private bool IsBrowsingSubfoldersAllowed(Outlook.Folder folder)
        {
            IgnoreListItem item = GetItem(folder);
            return item == null || item.getAllowRecursion();
        }

        private IgnoreListItem GetItem(Outlook.Folder folder)
        {
            foreach (IgnoreListItem ignoredFolder in ignoredFolders)
            {
                if (ignoredFolder.getName() == folder.Name.ToLower())
                {
                    return ignoredFolder;
                }
            }
            return null;
        }

        /// <summary>
        /// This method creates a list of string items, which represent the default folders in
        /// outlook to exclude them in the calculation of the classification.
        /// </summary>
        private void InitIgnoredFolders()
        {
            ignoredFolders = new List<IgnoreListItem>();

            foreach (Outlook.OlDefaultFolders folderType in
                     Outlook.OlDefaultFolders.GetValues(typeof(Outlook.OlDefaultFolders)))
            {
                // http://msdn.microsoft.com/en-us/library/office/bb208072%28v=office.12%29.aspx
                if (folderType == Outlook.OlDefaultFolders.olFolderInbox)
                {
                    AddToIgnoredFolders(folderType, true);
                }
                else
                {
                    AddToIgnoredFolders(folderType, false);
                }
            }
        }

        /// <summary>
        /// This method adds the specified default folder to the list of ignored folders.
        /// </summary>
        private void AddToIgnoredFolders(Outlook.OlDefaultFolders folderType, bool allowRecursion)
        {
            Outlook.MAPIFolder folder;
            try
            {
                folder = nameSpace.GetDefaultFolder(folderType);
                if (folder == null)
                {
                    Logger.Write("folder is null");
                }
                else
                {
                    ignoredFolders.Add(new IgnoreListItem(folder.Name.ToLower(), allowRecursion));
                }
            }
            catch (COMException ex)
            {
                // this folderType does not exist. don't worry and do nothing
                String message = "The default foldertype does not exist!";
                LoggerUtils.Write(message, ex, TraceEventType.Information);
            }
        }

        /// <summary>
        /// This method saves dataset to ARFF file.
        /// </summary>
        /// <param name="filePath">the destination path incl file name</param>
        /// <param name="isFilteredOutputToBeSaved">
        /// true, the arff file is filtered with the StringToWord filter
        /// false, the arff file isn't filtered, it's possible to read the content of it
        /// </param>
        public void SaveTrainingDataToFile(string filePath, bool isFilteredOutputToBeSaved)
        {
            if (isFilteredOutputToBeSaved)
            {
                textClassifierBuilder.SaveFiltered(filePath);
            }
            else
            {
                textClassifierBuilder.SaveUnfiltered(filePath);
            }
        }

        /// <summary>
        /// This method returns the Goodness of the selected classifier.
        /// </summary>
        /// <returns>goodness in percent</returns>
        public int GetGoodness()
        {
            return (int)Evaluation.pctCorrect();
        }

        /// <summary>
        /// This method returns the whole classifier test results.
        /// </summary>
        /// <returns>the whole testresults as a string.</returns>
        public string GetGoodnessExtra()
        {
            return Evaluation.toSummaryString("\nResults\n======\n", false)
                                     + "\n======\n *new random every App start!";
        }

        public void RefreshAllInThread()
        {
            Thread thread1 = new Thread(RefreshAllSafe);
            thread1.Start();
        }

        private void RefreshAllSafe()
        {
            try
            {
                RefreshAll();
            }
            catch (Exception ex)
            {
                LoggerUtils.Write("Error in RefreshAllSafe", ex, TraceEventType.Error);
            }
        }

        private void RefreshAll()
        {
            folderList = null;
            textClassifier = null;
            textClassifierBuilder = null;
            InitAndStoreAll();
            StoreAll();
        }

        private void StartCachingThread()
        {
            this.InitIgnoredFolders();
            LoadAllSafe();
            InitAndStoreAllSafe();
        }

        private void StartCallbackRegisteringThread()
        {
            RegisterCallbacksSafe();
        }

        private void LoadAllSafe()
        {
            try
            {
                LoadAll();
            }
            catch (Exception ex)
            {
                LoggerUtils.Write("Error in LoadAllSafe", ex, TraceEventType.Error);
            }
        }

        private void LoadAll()
        {
            textClassifier = Load<TextClassifier>(textClassifierFilename);
            folderList = Load<List<Folder>>(folderListFilename);
            textClassifierBuilder = Load<TextClassifierBuilder>(textClassifierBuilderFilename);
        }


        private void InitAndStoreAllSafe()
        {
            try
            {
                InitAndStoreAll();
            }
            catch (Exception ex)
            {
                LoggerUtils.Write("Error in InitAndStoreAllSafe", ex, TraceEventType.Error);
            }
        }

        
        private void InitAndStoreAll()
        {
            if (folderList == null)
            {
                // start over
                RefreshFolderList();
                StoreFolderList();
            }
            if (textClassifierBuilder == null)
            {
                textClassifierBuilder = new TextClassifierBuilder();
                textClassifierBuilder.SetClassifier(GetClassifer());
                textClassifierBuilder.SetClasses(from fl in folderList select fl.EntryID + " " + fl.StoreID);
                foreach (Folder folder in folderList)
                {
                    foreach (IClassifiable classifiable in folder)
                    {
                        textClassifierBuilder.AddTrainingInstance(classifiable.GetContent(), folder.EntryID + " " + folder.StoreID, 1);
                    }
                }
                textClassifierBuilder.FilterInstances();
                StoreTextClassifierBuilder();
            }
            if (textClassifier == null)
            {
                textClassifier = textClassifierBuilder.BuildClassifier();
                StoreTextClassifier();
            }
        }

        private void StoreAll()
        {
            StoreTextClassifier();
            StoreFolderList();
            StoreTextClassifierBuilder();
        }


        private void StoreTextClassifierBuilder()
        {
            Store(textClassifierBuilder, textClassifierBuilderFilename);
        }

        private void StoreTextClassifier() 
        {
            Store(textClassifier, textClassifierFilename);
        }
        
        private void StoreFolderList()
        {
            Store(folderList, folderListFilename);
        }

        private void Store(Object o, string filename)
        {
            BinaryFormatter formatter = new BinaryFormatter();
            String path = Path.Combine(Main.ApplicationDataPath, filename);
            FileStream stream = null;
            try
            {
                stream = new FileStream(path, FileMode.Create);
                formatter.Serialize(stream, o);

            }
            catch (IOException ex)
            {
                LoggerUtils.Write("Error (IOException) while storing", ex, TraceEventType.Error);
            }
            catch (SerializationException ex)
            {
                LoggerUtils.Write("Error (SerializationException) while storing", ex, TraceEventType.Error);
            }
            finally
            {
                if (stream != null)
                {
                    stream.Close();
                }
            }
        }

        private void RegisterCallbacksSafe()
        {
            try
            {
                foreach (Outlook.Store store in stores)
                {
                    Outlook.Folder rootFolder = store.GetRootFolder() as Outlook.Folder;
                    RegisterCallbacks(rootFolder);
                }
            }
            catch (Exception ex)
            {
                String message = "Error in RegisterCallbacksSafe(): It isn't possible to load mail data of Store";
                LoggerUtils.Write(message, ex, TraceEventType.Error);
            }
        }


        private T Load<T>(string filename)
        {
            BinaryFormatter formatter = new BinaryFormatter();
            String path = Path.Combine(Main.ApplicationDataPath, filename);
            Stream stream = null;
            try
            {
                stream = new FileStream(path, FileMode.Open);
                return (T)formatter.Deserialize(stream);
            }
            catch (IOException ex)
            {
                LoggerUtils.Write("Error (IOException) while loading", ex, TraceEventType.Error);
                // write to log
                return default(T);
            }
            catch (SerializationException ex)
            {
                LoggerUtils.Write("Error (SerializationException) while loading", ex, TraceEventType.Error);
                // write to log
                return default(T);
            }

            finally
            {
                if (stream != null)
                {
                    stream.Close();
                }
            }

        }

        private Classifier GetClassifer()
        {
            return ToClassifier(Settings.Default.classifierIndex);       
        }

        private Classifier ToClassifier(int classifierIndex)
        {
            switch (classifierIndex)
            {
                case 0:
                    return new NaiveBayesMultinomialUpdateable();
                case 1:
                    return new NaiveBayesMultinomial();
                case 2:
                    return new ComplementNaiveBayes();
                case 3:
                    return new DMNBtext();
                default:
                    return new NaiveBayesMultinomialUpdateable();
            }
        }

    }
}